// -----------------------------------------------------------
// Game Framework
// Copyright © 2024-2024 deadcycle. All rights reserved.
// Homepage: https://deadcycle.gitee.io
// Feedback: deadcycle@163.com
// -----------------------------------------------------------

using System;
using System.Collections.Generic;

using Cysharp.Threading.Tasks;

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace UIWidget
{
    /// <summary>
    /// 这是一个高功能按钮。相比于标准按钮，它有以下改进：
    /// 1. 添加脚本定义符号 DISALLOW_REFOCUS 将禁用在重新聚焦时进行单击判定。
    /// 2. 在编辑器中，修复了即使按钮在不可单击状态下按下空格键或回车键仍会被单击的问题。
    /// 2. 添加Press、Press-Repeat、Press-Hold事件。
    /// 4. 可以通过上下文菜单将现有的Button组件转换为ButtonEx。
    /// </summary>
    public class ButtonEx : Button
    {
        /// <summary> 选择回调方式。 </summary>
        [Flags]
        public enum EventType
        {
            /// <summary> 单击。 </summary>
            Click = 1 << 0,

            /// <summary> 按下。 </summary>
            Press = 1 << 1,

            /// <summary> 长按。 </summary>
            Hold = 1 << 2,
        }

        #region 静态变量
        /// <summary> PointDown响应限制时间。 </summary>
        private const float k_TimeIgnoreRapidPointDown = 0.2f;

        /// <summary> 点击事件响应限制时间。 </summary>
        private const float k_TimeIgnoreRapidClick = 0.2f;

        /// <summary> 上一次触发单击事件的帧数。 </summary>
        private static int s_LastFrameTrigger;

        /// <summary> 上一次触发PointDown的帧数。 </summary>
        private static int s_LastFramePointDown;

        /// <summary> 射线检测结果。 </summary>
        private static readonly List<RaycastResult> s_RaycastResult = new List<RaycastResult>();
        #endregion

        #region 序列化字段
        [SerializeField]
        private EventType m_EventType = EventType.Click;

        [SerializeField]
        private bool m_EnablePressRepeat;

        [SerializeField, Range(0.1f, 5.0f)]
        private float m_PressRepeatInterval = 1f;

        /// <summary> 长按时间的阈值。 </summary>
        [SerializeField, Range(0.1f, 5.0f)]
        private float m_HoldThreshold = 1;

        [SerializeField, Range(17, 1000)]
        private int m_PressCheckInterval = 100;
        #endregion

        #region 字段
        /// <summary> 鼠标指针是否在按钮内。 </summary>
        protected bool m_IsInside;

        /// <summary> 鼠标指针是否按下了按钮。 </summary>
        protected bool m_IsPress;

        /// <summary> 按下的持续时间。 </summary>
        protected float m_TimePressing;

        /// <summary> 重复按下等待时间。 </summary>
        private float m_TimeNextPress;

        private bool m_IsCheckingPress;

        /// <summary> 按下回调函数。 </summary>
        private UnityEvent m_OnPress;

        /// <summary> 长按回调函数。 </summary>
        private UnityEvent m_OnHold;

        private RectTransform m_CacheRectTransform;

        private Graphic m_Graphic;
        #endregion

        #region 属性
        /// <summary>
        /// 返回是否可点击。
        /// </summary>
        protected bool EnableClick =>
            isActiveAndEnabled &&
            interactable &&
            (m_EventType & EventType.Click) != 0 &&
            !IsRapidClick;

        /// <summary>
        /// 按钮点击回调是否连击触发。
        /// </summary>
        protected bool IsRapidClick =>
            Time.frameCount - s_LastFrameTrigger <=
            k_TimeIgnoreRapidClick *
            (Application.targetFrameRate <= 0 ? 30 : Application.targetFrameRate);

        /// <summary>
        /// PointDown是否连击触发
        /// </summary>
        protected bool IsRapidPointDown =>
            Time.frameCount - s_LastFramePointDown <=
            k_TimeIgnoreRapidPointDown *
            (Application.targetFrameRate <= 0 ? 30 : Application.targetFrameRate);

        public EventType ButtonEventType => m_EventType;

        /// <summary> 是否允许按下重复触发事件 (注意: 开启长按模式, 会受到影响)。 </summary>
        public bool EnablePressRepeat
        {
            get => m_EnablePressRepeat;
            set => m_EnablePressRepeat = value;
        }

        /// <summary> 重复间隔时间。 </summary>
        public float PressRepeatInterval
        {
            get => m_PressRepeatInterval;
            set => m_PressRepeatInterval = value;
        }

        /// <summary> 按下回调函数。 </summary>
        public UnityEvent OnPress => m_OnPress ??= new UnityEvent();

        /// <summary> 长按回调函数。 </summary>
        public UnityEvent OnHold => m_OnHold ??= new UnityEvent();

        /// <summary> 缓存RectTransform。 </summary>
        protected RectTransform CacheRectTransform =>
            m_CacheRectTransform ??= GetComponent<RectTransform>();

        private Canvas Canvas => m_Graphic ? m_Graphic.canvas : null;
        #endregion

        protected override void OnEnable()
        {
            base.OnEnable();

            m_Graphic = GetComponent<Graphic>();
        }

        protected override void OnDisable()
        {
            m_IsCheckingPress = false;

            base.OnDisable();
        }

        #region 处理输入交互
        /// <summary>
        /// 评估当前状态并转换到适当的状态。
        /// </summary>
        public override void OnPointerEnter(PointerEventData eventData)
        {
            base.OnPointerEnter(eventData);

            if (!m_IsPress)
            {
                // 按压的情况下, 如果指针离开按钮范围, 重新回到按钮范围, 不视为在范围内
                m_IsInside = true;
            }
        }

        /// <summary>
        /// 评估当前状态并转换到正常状态。
        /// </summary>
        public override void OnPointerExit(PointerEventData eventData)
        {
            base.OnPointerExit(eventData);

            m_IsInside = false;
        }

        /// <summary>
        /// 评估当前状态并转换到按下状态。
        /// </summary>s
        public override void OnPointerDown(PointerEventData eventData)
        {
            base.OnPointerDown(eventData);

            if (!isActiveAndEnabled || !interactable)
            {
                return;
            }

            if (IsRapidPointDown)
            {
                return;
            }

            s_LastFramePointDown = Time.frameCount;

            m_IsPress = true;
            m_IsInside = true;

            m_TimePressing = 0;
            m_TimeNextPress = m_TimePressing + m_PressRepeatInterval;

            if ((m_EventType & EventType.Press) != 0 && m_OnPress != null)
            {
                m_OnPress.Invoke();

                if (m_EnablePressRepeat)
                {
                    AsyncCheckPress().Forget();
                }
            }
            else if ((m_EventType & EventType.Hold) != 0 && m_OnHold != null)
            {
                AsyncCheckPress().Forget();
            }
        }

        /// <summary>
        /// 评估事件数据并转换到适当的状态。
        /// </summary>
        public override void OnPointerUp(PointerEventData eventData)
        {
            base.OnPointerUp(eventData);

            m_IsPress = false;
            m_TimePressing = 0;

#if DISALLOW_REFOCUS
            //[如果禁止重新获取焦点]
            // 在PointerUp 时检查是否可以点击:
            // * EventData 被判断为有效点击事件(eventData.eligibleForClick)
            // * 按钮松开时, 指针仍在按钮区域内
            // * 没有连续点击(不在连续点击的判断时长内)，且点击事件已启用
            if (eventData.eligibleForClick && m_IsInside && EnableClick)
            {
                ExecuteClick();
            }
#endif
        }

        /// <summary>
        /// 已注册 IPointerClickHandler 回调函数。
        /// </summary>
        public override void OnPointerClick(PointerEventData eventData)
        {
#if !DISALLOW_REFOCUS
            // [如果允许重新获取焦点（Unity默认）]
            // 在 PointerClick 时执行点击。
            if (EnableClick)
            {
                ExecuteClick();
            }
#endif
        }

        /// <summary>
        /// 已注册 ISubmitHandler 回调函数。
        /// </summary>
        public override void OnSubmit(BaseEventData eventData)
        {
            // 当决定按钮被按下，或者在编辑器中输入空格键等时，检查该按钮是否处于可按状态。
            if (EnableClick && IsClickable())
            {
                ExecuteClick();
            }
        }
        #endregion

        #region 处理点击相关
        /// <summary>
        /// 执行点击。
        /// </summary>
        protected virtual void ExecuteClick()
        {
            try
            {
                s_LastFrameTrigger = Time.frameCount;
                base.OnPointerClick(new PointerEventData(EventSystem.current));
            }
            catch (Exception e)
            {
                Debug.LogError(e);
            }
        }

        /// <summary>
        /// 实际进行射线投射以判断是否可以进行点击操作。
        /// </summary>
        private bool IsClickable()
        {
            //如果按钮处于不可按状态，则跳过该操作。
            if (!isActiveAndEnabled ||
                !interactable ||
                m_Graphic == null ||
                EventSystem.current == null)
            {
                return false;
            }

            PointerEventData eventData = new PointerEventData(EventSystem.current)
            {
                position = Coordinate.RectTransformToScreenPoint(Canvas, CacheRectTransform),
            };

            s_RaycastResult.Clear();
            EventSystem.current.RaycastAll(eventData, s_RaycastResult);

            if (s_RaycastResult.Count == 0)
            {
                // 没有击中任何物体
                s_RaycastResult.Clear();

                return false;
            }

            GameObject go = s_RaycastResult[0].gameObject;
            s_RaycastResult.Clear();

            // 射线击中的第一个对象是自身, 可触发点击
            if (go == gameObject)
            {
                return true;
            }

            // 射线击中的对象是自身的一个子图形组件, 且是该子图形组件的父级物体上第一个可交互组件, 可触发点击
            return go.GetComponent<Graphic>() &&
                go.transform.IsChildOf(transform) &&
                go.GetComponentInParent<Selectable>() == this;
        }
        #endregion

        /// <summary>
        /// 异步检测按钮按下。
        /// </summary>
        private async UniTaskVoid AsyncCheckPress()
        {
            if (m_IsCheckingPress)
            {
                return;
            }

            m_IsCheckingPress = true;

            do
            {
                float lastTime = Time.unscaledTime;

                // 检测扫描间隔
                await UniTask.Delay(m_PressCheckInterval, true);

                if (!interactable || !m_IsPress || !m_IsInside)
                {
                    m_IsCheckingPress = false;
                }

                // 已经停止检测了
                if (!m_IsCheckingPress)
                {
                    break;
                }

                // 执行按钮长按的检查。
                CheckPressHold();

                // 执行按钮按下重复的检查。
                CheckPressRepeat();

                if (!m_IsCheckingPress)
                {
                    break;
                }

                m_TimePressing += Time.unscaledTime - lastTime;
            } while (true);
        }

        /// <summary>
        /// 停止检测按钮按下。
        /// </summary>
        public void StopCheckPress()
        {
            m_IsCheckingPress = false;
        }

        /// <summary>
        /// 执行按钮按下重复的检查。
        /// </summary>
        private void CheckPressRepeat()
        {
            if (!m_EnablePressRepeat ||
                m_TimeNextPress >= m_TimePressing ||
                (m_EventType & EventType.Press) == 0)
            {
                return;
            }

            m_TimeNextPress = m_TimePressing + m_PressRepeatInterval;
            m_OnPress?.Invoke();
        }

        /// <summary>
        /// 执行按钮长按的检查。
        /// </summary>
        private void CheckPressHold()
        {
            if (m_HoldThreshold >= m_TimePressing || (m_EventType & EventType.Hold) == 0)
            {
                return;
            }

            // 停止检测按钮按下, 长按回调只能触发一次
            m_IsCheckingPress = false;

            // 视为松开了按钮
            m_IsInside = false;
            m_IsPress = false;
            m_TimePressing = 0;

            m_OnHold?.Invoke();
        }
    }
}